<!DOCTYPE html>
<html>
  <head>
    <title>WebGL Cube Multiple</title>
    <style>
      html, body { margin: 0; height: 100% }
      canvas { width: 100%; height: 100%; display: block; }

      #fail {
        position: fixed;
        left: 0;
        top: 0;
        width: 100%;
        height: 100%;
        display: flex;
        justify-content: center;
        align-items: center;
        background: red;
        color: white;
        font-weight: bold;
        font-family: monospace;
        font-size: 16pt;
        text-align: center;
      }
    </style>
  </head>
  <body>
    <canvas></canvas>
    <div id="fail" style="display: none">
      <div class="content"></div>
    </div>
  </body>
  <script type="module">
import {vec3, mat4} from '/3rdparty/wgpu-matrix.module.js';

function main() {
  const gl = document.querySelector('canvas').getContext('webgl');
  if (!gl) {
    fail('need webgl');
    return;
  }

  const vSrc = `
  uniform mat4 u_worldViewProjection;
  uniform mat4 u_worldInverseTranspose;

  attribute vec4 a_position;
  attribute vec3 a_normal;
  attribute vec2 a_texcoord;

  varying vec2 v_texCoord;
  varying vec3 v_normal;

  void main() {
    gl_Position = u_worldViewProjection * a_position;
    v_texCoord = a_texcoord;
    v_normal = (u_worldInverseTranspose * vec4(a_normal, 0)).xyz;
  }
  `;

  const fSrc = `
  precision highp float;

  varying vec2 v_texCoord;
  varying vec3 v_normal;

  uniform sampler2D u_diffuse;
  uniform vec3 u_lightDirection;

  void main() {
    vec4 diffuseColor = texture2D(u_diffuse, v_texCoord);
    vec3 a_normal = normalize(v_normal);
    float l = dot(a_normal, u_lightDirection) * 0.5 + 0.5;
    gl_FragColor = vec4(diffuseColor.rgb * l, diffuseColor.a);
  }
  `;

  function createBuffer(gl, data, type = gl.ARRAY_BUFFER) {
    const buf = gl.createBuffer();
    gl.bindBuffer(type, buf);
    gl.bufferData(type, data, gl.STATIC_DRAW);
    return buf;
  }

  const positions = new Float32Array([1, 1, -1, 1, 1, 1, 1, -1, 1, 1, -1, -1, -1, 1, 1, -1, 1, -1, -1, -1, -1, -1, -1, 1, -1, 1, 1, 1, 1, 1, 1, 1, -1, -1, 1, -1, -1, -1, -1, 1, -1, -1, 1, -1, 1, -1, -1, 1, 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1, -1, 1, -1, 1, 1, -1, 1, -1, -1, -1, -1, -1]);
  const normals   = new Float32Array([1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1]);
  const texcoords = new Float32Array([1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1]);
  const indices   = new Uint16Array([0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 8, 9, 10, 8, 10, 11, 12, 13, 14, 12, 14, 15, 16, 17, 18, 16, 18, 19, 20, 21, 22, 20, 22, 23]);

  const positionBuffer = createBuffer(gl, positions);
  const normalBuffer = createBuffer(gl, normals);
  const texcoordBuffer = createBuffer(gl, texcoords);
  const indicesBuffer = createBuffer(gl, indices, gl.ELEMENT_ARRAY_BUFFER);

  const tex = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, tex);
  gl.texImage2D(
      gl.TEXTURE_2D,
      0,    // level
      gl.RGBA,
      2,    // width
      2,    // height
      0,
      gl.RGBA,
      gl.UNSIGNED_BYTE,
      new Uint8Array([
        255, 255, 128, 255,
        128, 255, 255, 255,
        255, 128, 255, 255,
        255, 128, 128, 255,
      ]));
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

  function createShader(gl, type, source) {
    const sh = gl.createShader(type);
    gl.shaderSource(sh, source);
    gl.compileShader(sh);
    if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
      throw new Error(gl.getShaderInfoLog(sh));
    }
    return sh;
  }

  function createProgram(gl, vs, fs) {
    const prg = gl.createProgram();
    gl.attachShader(prg, vs);
    gl.attachShader(prg, fs);
    gl.linkProgram(prg);
    if (!gl.getProgramParameter(prg, gl.LINK_STATUS)) {
      throw new Error(gl.getProgramInfoLog(prg));
    }
    return prg;
  }

  const vs = createShader(gl, gl.VERTEX_SHADER, vSrc);
  const fs = createShader(gl, gl.FRAGMENT_SHADER, fSrc);

  const program = createProgram(gl, vs, fs);

  const u_lightDirectionLoc = gl.getUniformLocation(program, 'u_lightDirection');
  const u_diffuseLoc = gl.getUniformLocation(program, 'u_diffuse');
  const u_worldInverseTransposeLoc = gl.getUniformLocation(program, 'u_worldInverseTranspose');
  const u_worldViewProjectionLoc = gl.getUniformLocation(program, 'u_worldViewProjection');

  const positionLoc = gl.getAttribLocation(program, 'a_position');
  const normalLoc = gl.getAttribLocation(program, 'a_normal');
  const texcoordLoc = gl.getAttribLocation(program, 'a_texcoord');

  const numObjects = 100;
  const objectInfos = [];

  for (let i = 0; i < numObjects; ++i) {
    const across = Math.sqrt(numObjects) | 0;
    const x = (i % across - (across - 1) / 2) * 3;
    const y = ((i / across | 0) - (across - 1) / 2) * 3;

    objectInfos.push({
      translation: [x, y, 0],
    });
  }

  function resizeCanvasToDisplaySize(canvas) {
    const width = canvas.clientWidth;
    const height = canvas.clientHeight;
    const needResize = width !== canvas.width || height !== canvas.height;
    if (needResize) {
      canvas.width = width;
      canvas.height = height;
    }
    return needResize;
  }

  function render(time) {
    time *= 0.001;
    resizeCanvasToDisplaySize(gl.canvas);
    gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

    gl.enable(gl.DEPTH_TEST);
    gl.enable(gl.CULL_FACE);
    gl.clearColor(0.5, 0.5, 0.5, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

    gl.useProgram(program);

    const projection = mat4.perspective(30 * Math.PI / 180, gl.canvas.clientWidth / gl.canvas.clientHeight, 0.5, 100);
    const eye = [1, 4, -46];
    const target = [0, 0, 0];
    const up = [0, 1, 0];

    const view = mat4.lookAt(eye, target, up);
    const viewProjection = mat4.multiply(projection, view);

    gl.uniform3fv(u_lightDirectionLoc, vec3.normalize([1, 8, -10]));
    gl.uniform1i(u_diffuseLoc, 0);

    gl.activeTexture(gl.TEXTURE0);
    gl.bindTexture(gl.TEXTURE_2D, tex);

    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
    gl.vertexAttribPointer(positionLoc, 3, gl.FLOAT, false, 0, 0);
    gl.enableVertexAttribArray(positionLoc);

    gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
    gl.vertexAttribPointer(normalLoc, 3, gl.FLOAT, false, 0, 0);
    gl.enableVertexAttribArray(normalLoc);

    gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer);
    gl.vertexAttribPointer(texcoordLoc, 2, gl.FLOAT, false, 0, 0);
    gl.enableVertexAttribArray(texcoordLoc);

    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indicesBuffer);

    objectInfos.forEach(({translation}, ndx) => {
      const world = mat4.translation(translation);
      mat4.rotateX(world, time * 0.9 + ndx, world);
      mat4.rotateY(world, time + ndx, world);

      gl.uniformMatrix4fv(u_worldInverseTransposeLoc, false, mat4.transpose(mat4.inverse(world)));
      gl.uniformMatrix4fv(u_worldViewProjectionLoc, false, mat4.multiply(viewProjection, world));

      gl.drawElements(gl.TRIANGLES, 6 * 6, gl.UNSIGNED_SHORT, 0);
    });

    requestAnimationFrame(render);
  }
  requestAnimationFrame(render);
}

function fail(msg) {
  const elem = document.querySelector('#fail');
  const contentElem = elem.querySelector('.content');
  elem.style.display = '';
  contentElem.textContent = msg;
}

main();
  </script>
</html>